perf(console): free the declared-lazy marketplace routes, pin the metadata-admin surface that cannot be freed - #6775
Merged
Conversation
…adata-admin surface `AppContent` declares four surfaces with `lazy()` that the emitted bundle loaded eagerly anyway. Measured on `b98352a15` from `apps/console/dist/eager-closure.json` and the emitted chunks' own module lists (never a source-level search — objectui#6680 is the recorded reason the source graph cannot see this): metadata-admin 172,651 B gz eager -> unchanged, PINNED with the proof MarketplacePackagePage 7,647 B gz eager -> lazy MarketplaceInstalledPage 1,836 B gz eager -> lazy MarketplacePage already lazy -> lazy (the control) Both marketplace chunks were held by CHUNK CO-TENANCY, with no import edge to the page: rolldown had parked `components/SuggestedBindingsPanel.tsx` (statically imported by the eager `views/studio-design/StudioDesignSurface.tsx`) in one and `console/marketplace/InstalledListWidget.tsx` (bare-imported by the package barrel for its SDUI registration) in the other. The `app-shell-eager-leaves` group isolates those co-tenants. Eager closure 3180.2 KB -> 3171.5 KB gzipped (-8,888 B, 48 -> 45 eager chunks of 513), per-chunk ceilings unmoved. The opposite grouping was measured FIRST and was worse: a group over the three pages swept the bare-imported widget in with them, emitted a 44 KB eager chunk that swallowed `runtime-config.ts`, `MetadataProvider.tsx` and `@object-ui/plugin-form`, and netted -1,697 B while destroying three other lazy boundaries. The comment records it. `metadata-admin` is not freed. `views/metadata-admin/index.ts` is statically imported by the package barrel and by `services/builtinComponents.tsx`, which registers `MetadataDirectoryPage` and `MetadataResourceRouter` BY VALUE, and it performs five load-bearing top-level registrations. Its six pages have no dynamic importer of their own in the emitted graph, so no chunking policy separates them. `scripts/vite-declared-lazy-views.ts` now parses every RELATIVE `lazy()` specifier (12 files, up from 8) so the directory barrel and the marketplace routes are inside the ledger, and gains a second purity guard that reads the package's own `sideEffects` array: `bareSideEffectImport` returns null for `views/metadata-admin/index.ts` because its five side effects are CALLS, so without that guard deleting the pin would have declared it pure and dropped every built-in preview registration. Part of #6681 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-sales
marked this pull request as ready for review
August 29, 2026 11:09
This was referenced Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6681
Four surfaces
AppContentdeclares withlazy()and that the emitted bundle loaded onevery console page load anyway. Each was measured separately, from the emitted
chunk's own module list — never a source-level search, which cannot see the mechanism at
all (objectui#6680 is the recorded reason).
Baseline, re-measured on my own base
The card's pair of numbers is stale on both sides; both were re-read on
b98352a15:ece68882+ objectui#6535)b98352a15Which side moved: both, and by the same commit.
faac0d935(objectui#6735, thesideEffectsarray of objectui#6683) loweredMAX_EAGER_CLOSURE_GZIP_BYTESfrom3_345_000to3_300_000(−45,000 B) and took more load off than that(−51.5 KB), which is why headroom widened rather than narrowed. Recorded here for
objectui#6631, which remains open and is not touched by this PR.
Per chunk: the module list, the edge, and whether it was removable
metadata-adminMarketplacePackagePageMarketplaceInstalledPageMarketplacePageThe card measured
metadata-adminat 181,228 B andMarketplacePageat 3,129 B eager.Both moved:
metadata-adminis 8,577 B smaller, andMarketplacePageleft the eagerset entirely before this branch. That is a finding, not a nuisance — one of the four
was already fixed by objectui#6735, and it is exactly the control that makes the
co-tenancy mechanism legible below rather than a story about two files.
metadata-admin — 172,651 B, NOT removable
views/metadata-admin/index.tsis the target of all sixlazy()declarations, and theemitted graph shows it statically imported by two modules that are eager by
construction:
packages/app-shell/src/index.ts— the package barrel, re-exporting eleven runtimevalues from it (
registerMetadataPreview,useMetadataClient, …). The console entryimports that barrel.
packages/app-shell/src/services/builtinComponents.tsx— which the barrelBARE-imports (
import './services/builtinComponents.js';) and which importsMetadataDirectoryPageandMetadataResourceRouterby value, because a registryentry naming a component must hold the component.
Decisive for the chunking lever: the six pages the declarations name have
dynamicImporters: []in the emitted graph. They are reached only through thebarrel's static re-exports, so no
advancedChunkspolicy separates them from it. Themodule also performs five load-bearing top-level registrations
(
registerBuiltinAnchors,registerDefaultMetadataSchemas,registerDatasourceResource,registerBuiltinPreviews,registerBuiltinInspectors) — which is why@object-ui/app-shell's ownsideEffectsarray names it.Freeing it means changing what
registerAppComponentaccepts (a component VALUE today)and what the package barrel re-exports: a published-contract decision, not a bundling
one. It is pinned in
DECLARED_LAZY_VIEWS_STILL_EAGERwith that reasoning, and thefollow-up is filed separately.
The two marketplace chunks — chunk co-tenancy, removable
Neither page had a static importer. Their chunks did:
assets/MarketplacePackagePage-*.jsalso heldcomponents/SuggestedBindingsPanel.tsx(+
services/suggestedBindingsApi.ts), which the eagerviews/studio-design/StudioDesignSurface.tsximports statically.assets/MarketplaceInstalledPage-*.jsalso heldconsole/marketplace/InstalledListWidget.tsx, bare-imported by the barrel for its SDUIregistration.
The repair is an
app-shell-eager-leavesgroup inapps/console/vite.config.tsthatisolates those co-tenants, so all three pages chunk by their own dynamic-only
reachability.
The opposite grouping was measured FIRST and was worse. A
marketplace-routesgroupover the three declared-lazy pages swept the bare-imported widget in with them, so the
barrel's side-effect import made the GROUP eager; it became an attractor for 47 modules
including
runtime-config.ts,providers/MetadataProvider.tsxand@object-ui/plugin-form, emitting a 44 KB eager chunk. Net −1,697 B against −8,888 B,and three other lazy boundaries destroyed. Both the config comment and the plugin header
record it, because a chunk group decides co-tenancy and co-tenancy runs both ways.
Result
The ledger, widened
scripts/vite-declared-lazy-views.tsdeliberately parsed only../views/NAME.js.It now parses every relative
lazy()specifier — twelve distinct files, up fromeight — so the directory barrel and the marketplace routes are inside the ledger and
cannot drift back. The package specifier (
@object-ui/plugin-designer) stays out, withthe reason recorded: it needs a resolver to become a path, and its chunk was measured NOT
eager.
Widening the ledger meant widening what gets declared
moduleSideEffects: false, and theexisting purity guard was not enough for that.
bareSideEffectImportreads the modulesource for a bare
import './x.js';;views/metadata-admin/index.tshas no bareimport at all — its five side effects are CALLS — so that guard returns
nullfor theone file where a false purity claim would drop every built-in preview registration. A
second guard now reads the package's own published
sideEffectsarray and refuses anymodule it names. One producer of the fact, two consumers.
Verification
Union re-run on the final commit
d853fdedb, tree clean:Verdict lines, quoted from the gates themselves:
check:eager-closure—Console eager closure is 3171.5 KB gzipped across 45 of 513 chunks (budget: 3222.7 KB, headroom: 51.1 KB).check:sdui-registration-pins—All 12 registration(s) a sideEffects array promises are present in the built console (513 chunks weighed…), includingmarketplace:installed-list 1 chunk(s)— the widget my chunk group moved.check:side-effects-array—@object-ui/app-shell: sideEffects names exactly the 10 module(s) that register at load time…[plugin declared-lazy-views] 10/12 modules AppContent declares lazy are genuinely lazy; 2 eager, all pinnedvitest scripts/__tests__/vite-declared-lazy-views.test.ts+check-eager-closure-budget+console-vite-alias-closure-4925—Test Files 3 passed (3) · Tests 114 passed (114)pnpm --filter @object-ui/console type-check— exit 0 (tsc --noEmit && tsc -b tsconfig.node.json --forceechoed, so it ran)Reverse verification (ablation). The new purity guard was proved to fire by deleting
the
metadata-adminledger line and rebuilding. Mutation confirmed on disk before therun (
git hash-objecte1971ad3…→ae9bce55…, pin lines 1 → 0); build exited 1 inbuildStartwith[declared-lazy-views] refusing to declare … side-effect-free: its own package says otherwise; restored viagit checkout HEAD --and proved restored(
git diff HEADempty, blob hash back toe1971ad3…). Without that guard the deletionwould have declared the module pure.
Lint was narrowed and the narrowing is measured, not assumed: the diff's lintable
population is the three source files it changes (the fourth changed file is the
changeset, and
eslint.config.jsconfigures no markdown linting);--format jsonreports
files linted: 3, allerrorCount 0; andeslint.config.jsenables notype-aware linting (no
projectService, noparserOptions.project), so this diff cannotmove the verdict on any file it does not touch. The full farm runs in CI regardless.
Generated by Claude Code